using System;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Windows.Forms;
using System.Xml;
using JetBrains.ReSharper.UnitTestSupport.PluginInterface;
using log4net;

namespace JamesKovacs.ReSharper.UnitTestSupport.VstsUnit {
    [UnitTestAssemblyExplorer("VstsUnit")]
    public class VstsUnitTestAssembly : UnitTestAssemblyBase {
        private static readonly ILog logger = LogManager.GetLogger(typeof(VstsUnitTestAssembly));

        private const string resultsFileName = "__VstsUnitTestResultsFile__.trx";
        private const string xpathTestResult = "/Tests/UnitTestResult[testName/text() = '{0}']";
        private const string xpathOutcome = "outcome/value__";
        private const string xpathStdout = "stdout";
        private const string xpathErrorInfoMessage = "errorInfo/message";
        private const string xpathErrorInfoStackTrace = "errorInfo/stackTrace";
        private const string xpathStderr = "stderr";
        private const string xpathDebugTrace = "debugTrace";
        private const string xpathDuration = "duration";
        private string m_UnitTestAssemblyFullPath;
        private string m_UnitTestAssemblyName;
        private string m_WorkingDirectory;
        private string m_ResultsFileFullPath;
        private string m_RunnerId;

        public VstsUnitTestAssembly(string assemblyToLoad, string remotingUrl, string runnerID) : base(assemblyToLoad, remotingUrl, runnerID) {
            VstsUnitSetup.Initialize();
            
            m_UnitTestAssemblyFullPath = assemblyToLoad;
            m_UnitTestAssemblyName = Path.GetFileNameWithoutExtension(assemblyToLoad);
            m_WorkingDirectory = Path.GetDirectoryName(assemblyToLoad);
            m_ResultsFileFullPath = Path.Combine(m_WorkingDirectory, resultsFileName);
            m_RunnerId = runnerID;
        }

        public override void Run(string fixtureName, string[] methodNames, bool[] explicitFlags) {
            if(string.IsNullOrEmpty(fixtureName)) {
                throw new ArgumentNullException("fixtureName");
            }
            if(methodNames == null || methodNames.Length == 0) {
                throw new ArgumentNullException("methodNames");
            }
            if(explicitFlags == null || methodNames.Length != explicitFlags.Length) {
                throw new ArgumentException("methodNames[] and explicitFlags[] must be the same length", "explicitFlags");
            }
            try {
                if(logger.IsDebugEnabled) {
                    StringBuilder sb = new StringBuilder();
                    sb.AppendFormat("Fixture: {0}{1}", fixtureName, Environment.NewLine);
                    for(int i = 0; i < methodNames.Length; i++) {
                        sb.AppendFormat("{0} ({1}){2}", methodNames[i], explicitFlags[i], Environment.NewLine);
                    }
                    logger.Debug(sb.ToString());
                }
                CleanupRun();
                RunMSTest(fixtureName, methodNames);
                ParseAndDisplayResultsFile(fixtureName, methodNames);
                CleanupRun();
            } catch(Exception ex) {
                logger.Error("Error running unit tests", ex);
                MessageBox.Show(ex.ToString(), "Error Running Unit Tests");
            }
        }

        private void CleanupRun() {
            DirectoryInfo workingDirectory = new DirectoryInfo(m_WorkingDirectory);
            bool inFound = false;
            bool outFound = false;
            foreach(DirectoryInfo dirInfo in workingDirectory.GetDirectories()) {
                foreach(DirectoryInfo subdirInfo in dirInfo.GetDirectories()) {
                    switch(subdirInfo.Name) {
                        case "In":
                            inFound = true;
                            break;
                        case "Out":
                            outFound = true;
                            break;
                    }
                    if(inFound && outFound) {
                        dirInfo.Delete(true);
                    }
                }
            }

            if(File.Exists(m_ResultsFileFullPath)) {
                try {
                    File.Delete(m_ResultsFileFullPath);
                } catch(IOException ex) {
                    throw new IOException(string.Format("Unable to delete temporary results file, {0}, because it is currently in use.", m_ResultsFileFullPath), ex);
                }
            }
        }

        private void RunMSTest(string fixtureName, string[] methodNames) {
            if(string.IsNullOrEmpty(fixtureName)) {
                throw new ArgumentNullException("fixtureName");
            }
            if(methodNames == null || methodNames.Length == 0) {
                throw new ArgumentNullException("methodNames");
            }
            using(Process msTestProcess = new Process()) {
                ProcessStartInfo processStartInfo = new ProcessStartInfo(GetMSTestPath(), BuildMSTestParameters(methodNames));
                processStartInfo.WorkingDirectory = m_WorkingDirectory;
                processStartInfo.UseShellExecute = false;
                processStartInfo.WindowStyle = ProcessWindowStyle.Hidden;
                processStartInfo.CreateNoWindow = true;
                msTestProcess.StartInfo = processStartInfo;
                msTestProcess.Start();
                if(methodNames.Length == 1) {
                    NotifyTestStartup(fixtureName, methodNames[0]);
                } else {
                    NotifyFixtureStartup(fixtureName);
                }
                msTestProcess.WaitForExit();
            }
        }

        private void NotifyFixtureStartup(string fixtureName) {
            Listener.OnFixtureStartup(m_RunnerId, m_UnitTestAssemblyName, fixtureName);
        }

        private void NotifyTestStartup(string fixtureName, string methodName) {
            Listener.OnTestStarted(m_RunnerId, m_UnitTestAssemblyName, fixtureName, methodName);
        }

        private void ParseAndDisplayResultsFile(string fixtureName, string[] methodNames) {
            TestResult[] testResults = new TestResult[methodNames.Length];
            XmlDocument xmlResults = new XmlDocument();
            // The file might not exist if all tests to be run were marked with Ignore.
            // If this happens, just follow normal code path with empty XmlDocument.
            if(File.Exists(m_ResultsFileFullPath)) {
                xmlResults.Load(m_ResultsFileFullPath);
            }
            for(int i = 0; i < methodNames.Length; i++) {
                string methodName = methodNames[i];
                NotifyTestStartup(fixtureName, methodName);
                XmlNode testResultNode = xmlResults.SelectSingleNode(string.Format(xpathTestResult, methodName));
                TimeSpan duration;
                // MSTest ignored the test likely due to the Ignore attribute
                if(testResultNode == null) {
                    testResults[i] = TestResult.SKIPPED;
                    duration = new TimeSpan(0);
                } else {
                    NotifyConsole(testResultNode.SelectSingleNode(xpathStdout), TestOutputType.STDOUT);
                    NotifyConsole(testResultNode.SelectSingleNode(xpathErrorInfoMessage), TestOutputType.STDERR);
                    NotifyConsole(testResultNode.SelectSingleNode(xpathErrorInfoStackTrace), TestOutputType.STDERR);
                    NotifyConsole(testResultNode.SelectSingleNode(xpathStderr), TestOutputType.STDERR);
                    NotifyConsole(testResultNode.SelectSingleNode(xpathDebugTrace), TestOutputType.DEBUGTRACE);
                    duration = TimeSpan.Parse(testResultNode.SelectSingleNode(xpathDuration).InnerXml);
                    TestOutcome outcome = (TestOutcome)Enum.Parse(typeof(TestOutcome), testResultNode.SelectSingleNode(xpathOutcome).InnerXml);
                    testResults[i] = ConvertToJetbrainsTestResult(outcome);
                }
                NotifyTestFinished(fixtureName, methodName, testResults[i], string.Empty, duration.TotalMilliseconds);
            }
            NotifyFixtureTeardown(fixtureName);
            FixtureTestResult fixtureResult = CombineTestResults(testResults);
            NotifyFixtureFinished(fixtureName, fixtureResult);
        }

        private void NotifyConsole(XmlNode xmlNode, TestOutputType testOutputType) {
            if(xmlNode == null) {
                return;
            }
            string output = xmlNode.InnerXml;
            if(!string.IsNullOrEmpty(output)) {
                if(!output.EndsWith(Environment.NewLine)) {
                    output += Environment.NewLine;
                }
                Listener.OnConsole(m_RunnerId, m_UnitTestAssemblyName, output, testOutputType);
            }
        }

        private void NotifyTestFinished(string fixtureName, string methodName, TestResult testResult, string message, double time) {
            Listener.OnTestFinished(m_RunnerId, m_UnitTestAssemblyName, fixtureName, methodName, testResult, message, time);
        }

        private static FixtureTestResult CombineTestResults(TestResult[] testResults) {
            bool foundSkippedTest = false;
            foreach(TestResult testResult in testResults) {
                switch(testResult) {
                    case TestResult.ERROR:
                    case TestResult.FAILED:
                        // one failed test marks fixture as failed
                        return FixtureTestResult.FAILED;
                    case TestResult.SKIPPED:
                        foundSkippedTest = true;
                        break;
                }
            }
            return foundSkippedTest ? FixtureTestResult.SKIPPED : FixtureTestResult.SUCCESS;
        }

        private static TestResult ConvertToJetbrainsTestResult(TestOutcome outcome) {
            TestResult jetbrainsResult;
            switch(outcome) {
                case TestOutcome.Passed:
                case TestOutcome.PassedButRunAborted:
                    jetbrainsResult = TestResult.SUCCESS;
                    break;
                case TestOutcome.Error:
                case TestOutcome.Timeout:
                case TestOutcome.Aborted:
                    jetbrainsResult = TestResult.ERROR;
                    break;
                case TestOutcome.Failed:
                    jetbrainsResult = TestResult.FAILED;
                    break;
                case TestOutcome.NotExecuted:
                    jetbrainsResult = TestResult.SKIPPED;
                    break;
                case TestOutcome.Completed:
                case TestOutcome.Disconnected:
                case TestOutcome.InProgress:
                case TestOutcome.Inconclusive:
                case TestOutcome.NotRunnable:
                case TestOutcome.Pending:
                case TestOutcome.Warning:
                default:
                    throw new ApplicationException(string.Format("Unexpected TestOutcome: {0}", outcome));
            }
            return jetbrainsResult;
        }

        private void NotifyFixtureTeardown(string fixtureName) {
            Listener.OnFixtureTeardown(m_RunnerId, m_UnitTestAssemblyName, fixtureName);
        }

        private void NotifyFixtureFinished(string fixtureName, FixtureTestResult fixtureResult) {
            Listener.OnFixtureFinished(m_RunnerId, m_UnitTestAssemblyName, fixtureName, fixtureResult, string.Empty);
        }

        private static string GetMSTestPath() {
            string vsTools = Environment.GetEnvironmentVariable("VS80COMNTOOLS");
            if(string.IsNullOrEmpty(vsTools)) {
                throw new ApplicationException("Cannot locate MSTest.exe. VS80COMNTOOLS environment variable is not set.");
            }
            string msTestPath = Path.GetFullPath(Path.Combine(vsTools, @"..\IDE\MSTest.exe"));
            if(!File.Exists(msTestPath)) {
                throw new ApplicationException(string.Format("Cannot locate MSTest.exe. Expected location is {0}", msTestPath));
            }
            logger.Debug(msTestPath);
            return msTestPath;
        }

        private string BuildMSTestParameters(string[] testNames) {
            // mstest /nologo /testcontainer:bin\debug\JamesKovacs.ExamplesContractManagerTests.dll /resultsfile:bin\debug\results.trx /test:AddMoney /test:AllocateMoney
            // N.B. Do not use /unique as mstest.exe will then skip some tests with the same root name. 
            //      e.g. Two tests: ShouldCalculatePayments and ShouldCalculatePaymentsWithInterest. 
            //      If /unique is specified, only ShouldCalculatePaymentsWithInterest will be called.
            // N.B. Do not use /noisolation to speed up tests as configuration files (app.config) will not be loaded.
            StringBuilder sb = new StringBuilder();
            sb.AppendFormat("/nologo /testcontainer:\"{0}\" /resultsfile:{1}", m_UnitTestAssemblyFullPath, resultsFileName);
            foreach(string testName in testNames) {
                sb.AppendFormat(" /test:{0}", testName);
            }
            string msTestParameters = sb.ToString();
            logger.Debug(msTestParameters);
            return msTestParameters;
        }
    }
}